Skip to content

MONGOID-5057 Add support for has_many :through#6151

Open
jamis wants to merge 21 commits into
mongodb:masterfrom
jamis:5057-has_many-through
Open

MONGOID-5057 Add support for has_many :through#6151
jamis wants to merge 21 commits into
mongodb:masterfrom
jamis:5057-has_many-through

Conversation

@jamis
Copy link
Copy Markdown
Contributor

@jamis jamis commented May 26, 2026

Summary

Implements read-only :through associations for has_one and has_many, matching ActiveRecord's API:

class Customer
  has_one :franchise
  has_one :store, through: :franchise
end

class Physician
  has_many :appointments
  has_many :patients, through: :appointments
end

Both association types support :source to override the inferred source name, and :class_name for non-conventional class names. has_many :through additionally accepts :order. All through associations are read-only; write attempts raise Mongoid::Errors::ReadonlyAssociation. Eager loading via includes is supported for both types.

The two query patterns supported for has_many :through are the join-model pattern (FK on the intermediate, e.g. belongs_to :patient on Appointment) and the reverse-FK pattern (FK on the target, e.g. has_many :readers on Book).

Out of Scope

  • write operations
  • polymorphic through associations
  • custom :scope
  • embedded through associations
  • chaining through a has_many :through that is itself a through association
  • has_and_belongs_to_many as the source association.

jamis added 18 commits May 4, 2026 13:12
…ions

When eager_load is called with a HasOneThrough or HasManyThrough association,
log a warning naming the through associations and fall back to the includes-style
preload path, since $lookup does not support two-hop through queries.
…oped criteria

The metadata-level criteria(base) method must mirror the contract
established by HasMany and other associations: it takes the owner
document and returns a Criteria scoped to that owner's related records.
The previous split into an unscoped criteria/0 and a resolve(base)
method meant that any caller using the standard Relatable interface
(e.g. eager loaders) would receive an unscoped result.

Also add sum/avg/min/max to the proxy's delegator list so that
aggregation methods work consistently with other has_many proxies.
Copilot AI review requested due to automatic review settings May 26, 2026 16:32
@jamis jamis requested a review from a team as a code owner May 26, 2026 16:32
@jamis jamis requested a review from comandeo-mongo May 26, 2026 16:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds read-only :through support to Mongoid referenced associations, enabling has_one :through and has_many :through with includes preloading support and a dedicated Mongoid::Errors::ReadonlyAssociation error for mutation attempts.

Changes:

  • Introduces new association metadata/proxy/eager-loader implementations for Referenced::HasOneThrough and Referenced::HasManyThrough.
  • Extends association macro dispatch to select *_through association types when through: is provided.
  • Adds a localized ReadonlyAssociation error and updates eager loading behavior to fall back from $lookup when through associations are included.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
spec/mongoid/errors/readonly_association_spec.rb Adds specs for the new ReadonlyAssociation error messaging.
spec/mongoid/association/referenced/has_one_through/proxy_spec.rb Tests has_one-through proxy behavior and eager loader selection.
spec/mongoid/association/referenced/has_one_through/eager_spec.rb Verifies includes-based preloading for has_one-through.
spec/mongoid/association/referenced/has_one_through_spec.rb Adds unit + integration coverage for has_one-through metadata and read-only setter.
spec/mongoid/association/referenced/has_many_through/proxy_spec.rb Tests has_many-through proxy read/mutation API surface.
spec/mongoid/association/referenced/has_many_through/eager_spec.rb Verifies includes-based preloading for has_many-through.
spec/mongoid/association/referenced/has_many_through_spec.rb Adds unit + integration coverage for has_many-through metadata, ordering, and patterns.
spec/mongoid/association/macros_spec.rb Ensures macros instantiate the correct *_through metadata when through: is present.
spec/mongoid/association/eager_loadable_spec.rb Confirms eager_load warns and falls back to preload for through associations.
lib/mongoid/errors/readonly_association.rb Implements new error class used for write attempts on through associations.
lib/mongoid/errors.rb Requires the new error class.
lib/mongoid/association/referenced/has_one_through/proxy.rb Adds proxy class for has_one-through association type.
lib/mongoid/association/referenced/has_one_through/eager.rb Adds two-query preloader for has_one-through.
lib/mongoid/association/referenced/has_one_through.rb Adds metadata/definition logic for has_one-through.
lib/mongoid/association/referenced/has_many_through/proxy.rb Adds read-only proxy wrapper for has_many-through criteria.
lib/mongoid/association/referenced/has_many_through/eager.rb Adds two-query preloader for has_many-through.
lib/mongoid/association/referenced/has_many_through.rb Adds metadata/definition logic for has_many-through criteria building and ids getter.
lib/mongoid/association/referenced.rb Requires the new through association implementations.
lib/mongoid/association/macros.rb Routes has_one/has_many to through-metadata when through: is provided.
lib/mongoid/association/eager_loadable.rb Detects through inclusions and falls back from $lookup eager loading with a warning.
lib/mongoid/association.rb Adds internal THROUGH_MACRO_MAPPING for has_one/has_many.
lib/config/locales/en.yml Adds i18n strings for the new readonly_association error.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/mongoid/association/referenced/has_one_through.rb Outdated
Comment thread lib/mongoid/association/referenced/has_one_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_one_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_many_through.rb
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb
Comment thread lib/mongoid/association/referenced/has_one_through.rb
Comment thread lib/mongoid/association/referenced/has_one_through.rb
Comment thread lib/mongoid/association/referenced/has_many_through.rb
jamis added 3 commits May 26, 2026 11:23
- Remove :scope from ASSOCIATION_OPTIONS on both through types; it was
  listed as valid but never applied, causing silent no-ops
- Add HABTM guard to HasOneThrough#source_association to match the
  existing guard in HasManyThrough
- Fix reverse-FK eager loaders (has_one and has_many) to use
  source_assoc.primary_key when collecting intermediate PKs and joining
  back to targets; through_assoc.primary_key is the owner's key and
  gives wrong results when a custom :primary_key is configured
- Fix HasManyThrough#criteria reverse-FK branch with the same correction
- Eager loaders now store a Proxy rather than a raw document/array, so
  the cached type after includes() matches the non-eager getter
- HasManyThrough::Proxy accepts a preloaded: kwarg; enumerable methods
  (each, to_a, first, etc.) use the preloaded array when set, while
  query methods (where, pluck, etc.) always build a fresh Criteria
…hens

RSpec embeds context description strings in the JSON output. Em-dash
characters (U+2014, encoded as \xE2\x80\x94 in UTF-8) cause
Encoding::InvalidByteSequenceError when CI reads tmp/rspec.json under
LANG=C, where Encoding.default_external is US-ASCII.
@jamis jamis added the feature Adds a new feature, without breaking compatibility label May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds a new feature, without breaking compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants